Skip to content

Skip the dense copy of the sparse arguments in a sparse-plus-scalar broadcast such as A .+ 1 - #1105

Merged
ViralBShah merged 3 commits into
mainfrom
vs/densesum-scatter
Oct 20, 2026
Merged

ViralBShah merged 3 commits into
mainfrom
vs/densesum-scatter

Conversation

@ViralBShah

@ViralBShah ViralBShah commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Since #1004, a broadcast sum of a sparse array and a scalar, such as A .+ 1, is dense. It gets there by copying every sparse argument into a dense array, then broadcasting over the copies into a second dense array. For a sparse argument with few stored entries, that copy is half the memory and more than half the time.

Before (main; random n×n sparse A, B of density d; minimum time over 20 runs)

n, d A .+ 1 A .+ B .- 2
1000, 0.001 2.11 ms, 15.3 MiB 3.67 ms, 22.9 MiB
1000, 0.01 2.20 ms, 15.3 MiB 3.65 ms, 22.9 MiB
5000, 0.001 34.3 ms, 381.5 MiB 60.5 ms, 572.2 MiB
5000, 0.01 34.8 ms, 381.5 MiB 62.4 ms, 572.2 MiB
2000, 0.03 6.04 ms, 61.0 MiB 9.62 ms, 91.6 MiB
2000, 0.5 7.42 ms, 61.0 MiB 12.4 ms, 91.6 MiB

After

n, d A .+ 1 A .+ B .- 2
1000, 0.001 0.87 ms, 7.6 MiB 0.96 ms, 7.6 MiB
1000, 0.01 1.05 ms, 7.6 MiB 3.67 ms, 22.9 MiB
5000, 0.001 14.6 ms, 190.7 MiB 16.2 ms, 190.7 MiB
5000, 0.01 22.2 ms, 190.7 MiB 61.9 ms, 572.2 MiB
2000, 0.03 3.85 ms, 30.5 MiB 9.51 ms, 91.6 MiB
2000, 0.5 7.45 ms, 61.0 MiB 12.3 ms, 91.6 MiB

Sums with a dense array, such as A .+ M and v .+ M, are unchanged.

The new path evaluates the broadcast once with each sparse argument replaced by its implicit zero. That fills every entry of the result that no stored entry reaches. It then evaluates the broadcast again only at the entries a stored entry is broadcast to. The function is a tree of + and -, so evaluating it twice at an entry is unobservable. Results match dense broadcast in type and isequal, including NaN and -0.0. I checked that against dense on 36 expressions over Float64, ComplexF64, Bool and Int entries, with sparse vectors and 1-row and 1-column matrices broadcast across the result.

Each re-evaluated entry looks up every sparse argument. So the new path runs only while the entries reached, times the number of sparse arguments, stay below 1/32 of the result; otherwise the dense copy is cheaper. Above that density, as in the 0.5 row and for A .+ B .- 2 at 0.01, the old path runs.

Left out:

  • Sums with a dense array argument. Measured near the cutoff, the copy was not their main cost, and the new path was slower.
  • Adjoints, transposes and views of sparse arrays, which are still copied.
  • Element types that are not Number, and abstract ones such as Real. Their implicit zero can have another type than the result that dense broadcast infers from the values, so the old path runs whenever the two types differ.

🤖 Generated with Claude Code

…roadcast such as `A .+ 1`

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.67347% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.77%. Comparing base (a49c313) to head (d5f299f).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/higherorderfns.jl 83.67% 8 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1105      +/-   ##
==========================================
- Coverage   94.83%   94.77%   -0.06%     
==========================================
  Files          16       16              
  Lines       10252    10299      +47     
==========================================
+ Hits         9722     9761      +39     
- Misses        530      538       +8     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

ViralBShah and others added 2 commits October 20, 2026 07:49
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ult element type

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ViralBShah
ViralBShah merged commit ed2edcc into main Oct 20, 2026
12 checks passed
@ViralBShah
ViralBShah deleted the vs/densesum-scatter branch October 20, 2026 18:51
@ViralBShah ViralBShah mentioned this pull request Oct 27, 2026
30 tasks done
ViralBShah added a commit that referenced this pull request Oct 27, 2026
Backports of the merged PRs labelled `backport 1.14`, cherry-picked with
`-x` in the order they were merged on `main`. `release-1.14` starts at
b0aa455 (#1104), the commit JuliaLang/julia's `release-1.14` pins, and
every PR merged on `main` since then is labelled, so this branch's tree
is identical to `main`'s at 9bfad27.

- [x] #1105
- [x] #1011
- [x] #1107
- [x] #1108
- [x] #1110
- [x] #1111
- [x] #1114
- [x] #1112
- [x] #1115
- [x] #1117
- [x] #1118
- [x] #1109
- [x] #1113
- [x] #1122
- [x] #1123
- [x] #1120
- [x] #1124
- [x] #1125
- [x] #1116
- [x] #1121
- [x] #1126
- [x] #1128
- [x] #1130
- [x] #1131
- [x] #1119
- [x] #1133
- [x] #1127
- [x] #1135
- [x] #1134
- [x] #1137

#1137 removes the first-call compile time that #1128 added, which the
TTFX check flagged on this PR.

#1132 merged into the branch of #1131 and is part of its commit.

Not bug fixes: #1011 adds a deprecation, #1109 changes the `sprand`
value stream for a value function, #1113 changes the result shape of
`SparseMatrixCSC` of a vector view, and #1119, #1133, #1134 and #1135
add methods for views. They are included because they are labelled and
1.14 is not released yet.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Viral B. Shah <ViralBShah@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant